package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
)

const (
	INFO = `Info:
Name:        gosc-cli (go Server Chan cli)
Description: go sc cli client
Author:      secondriver <secondriver@yeah.net>
Version:     1.0
Website:     http://sc.ft.qq.com/
`
	DEFAULT_SCKEY = "SCU430Tbf96a97c707b17b08f53ece3fb438a9e55f666cb780ee"
	SCURL         = "http://sc.ftqq.com/${SCKEY}.send"
)

const (
	textLimit = 256
	despLimit = 1 << 16
	sckeyfile = "sckey.ini"
)

var commandProcessorMap = map[string]commandProcessor{
	"send":  &send{},
	"info":  &info{},
	"sckey": &sckey{},
	"help":  &help{},
}

var (
	ptext  string
	pdesp  string
	psckey string
	pfile  string
)

//Process command line params variables
func init() {

	flag.StringVar(&ptext, "text", "", "SC send info `title content`, limit "+strconv.Itoa(textLimit)+" byte  [text and file only one].")
	flag.StringVar(&pdesp, "desp", "", "SC send info `main content` , support markdown and limit "+strconv.Itoa(despLimit/(1<<10))+"KB [optional].")
	flag.StringVar(&psckey, "sckey", "", "SC send info certificate `key` .")
	flag.StringVar(&pfile, "file", "", "SC send info read from 'file', [text and file only one] \n"+
		"\t\tthe file format is (exclude double quote):\n"+
		"\t\t\"\n"+
		"\t\ttext:\n"+
		"\t\t'This is title content.'\n"+
		"\t\tdesp:\n"+
		"\t\t'This is main content.'\n"+
		"\t\t\"")

	var commandFormat [][]string = [][]string{
		{"*params*", "*command*", "*description*"},
		{"", "help, h, ?", "show help info"},
		{"[-sckey value]", "sckey", "show sckey from file or set sckey store to file"},
		{"", "info", "application information"},
		{"-text value [-desp value] [-sckey value] ", "send", "send message to webchat"},
		{"-file value [-sckey value]", "send", "send message from file to webchat"},
	}

	var maxwidth = func(index int) int {
		var max = 0
		for _, row := range commandFormat {
			if n := len(row[index]); n > max {
				max = n
			}
		}
		return max
	}

	var padding = 4
	var clo1maxwidth = maxwidth(0) + padding
	var clo2maxwidth = maxwidth(1) + padding

	flag.Usage = func() {
		var app = filepath.Base(os.Args[0])
		fmt.Printf("Usage of %s :\n", app)
		fmt.Println("Commands : info, send, sckey")
		fmt.Println("Command Format:")
		for _, row := range commandFormat {
			fmt.Print("  ", row[0])
			for padding := clo1maxwidth - len(row[0]); padding > 0; padding-- {
				fmt.Print(" ")
			}
			fmt.Print(row[1])
			for padding := clo2maxwidth - len(row[1]); padding > 0; padding-- {
				fmt.Print(" ")
			}
			fmt.Println(row[2])
		}
		fmt.Println("Params:")
		flag.PrintDefaults()
	}
}

var (
	userhome          string
	sckeyfilePathName string
)

//Process sckey file path and name
func init() {
	if user, err := user.Current(); err == nil {
		userhome = user.HomeDir
	}
	if userhome != "" {
		sckeyfilePathName = filepath.FromSlash(filepath.Join(userhome, sckeyfile))
	} else {
		sckeyfilePathName = filepath.FromSlash(sckeyfile)
	}
}

func main() {
	flag.Parse()

	if flag.NArg() != 1 {
		flag.Usage()
	} else {
		var command = flag.Arg(0)
		if process, ok := commandProcessorMap[command]; ok {
			err := process.handler()
			if err != nil {
				fmt.Println("(: ERROR:", err.Error())
				flag.Usage()
			}
		} else {
			fmt.Println("(: ERROR: Not found command ", command)
			flag.Usage()
		}
	}
	os.Exit(0)
}

//Check text isn't coincidence requirements
func checkText(text string) error {
	if len([]byte(ptext)) > textLimit {
		return errors.New(fmt.Sprintf("title(text) content too long more than %s byte .\n", strconv.Itoa(textLimit)))
	}
	return nil
}

//Check desp isn't coincidence requirements
func checkDesp(desp string) error {
	if len([]byte(pdesp)) > despLimit {
		return errors.New(fmt.Sprintf("main(desp) content too long more than %s KB .\n", strconv.Itoa(despLimit/(1<<10))))
	}
	return nil
}

//Read SC send content from file
func readScInfoFile(fileName string) (text, desp string, err error) {
	if file, err := os.Open(filepath.FromSlash(fileName)); err == nil {
		defer file.Close()
		reader := bufio.NewReader(file)
		var textBuff bytes.Buffer
		var despBuff bytes.Buffer
		var tag = 0
		for {
			line, err := reader.ReadString('\n')
			if err != nil {
				if err == io.EOF {
					break
				}
			}
			if strings.TrimSpace(line) == "title:" {
				if tag != 1 {
					tag = 1
					continue
				}
			}
			if strings.TrimSpace(line) == "desp:" {
				if tag != 2 {
					tag = 2
					continue
				}
			}
			if tag == 1 {
				textBuff.WriteString(line)
			}
			if tag == 2 {
				despBuff.WriteString(line)
			}
		}
		text = textBuff.String()
		desp = despBuff.String()
		return text, desp, err
	} else {
		return "", "", errors.New(fmt.Sprintf("Open file %s failed .", pfile))
	}
}

//Read SC sckey from file
func readsckey() (string, error) {
	file, err := os.Open(sckeyfilePathName)
	defer file.Close()
	if err != nil {
		return "", errors.New(fmt.Sprintf("Open Your local %s file failed.\n ", sckeyfilePathName))
	} else {
		reader := bufio.NewReader(file)
		line, _, err := reader.ReadLine()
		if err != nil {
			return "", errors.New(fmt.Sprintf("Read Your local %s sc's sckey failed.\n", sckeyfilePathName))
		} else {
			return string(line), nil
		}
	}
}

//Update SC sckey to file
func writesckey(sckey string) (err error) {
	err = ioutil.WriteFile(sckeyfilePathName, []byte(sckey), 666)
	return
}

type commandProcessor interface {
	handler() error
}

type ScMessage struct {
	textData string `des:"Message title"`
	despData string `des:"Message main content support markdown"`
	sckey    string `des:"Send message used sckey"`
}

type scRespone struct {
	//	{"errno":0,"errmsg":"success","dataset":"done"}
	Errno   int    `json:"errno"`
	Errmsg  string `json:"errmsg"`
	Dataset string `json:"dataset"`
}

func (this *scRespone) toString() string {
	return fmt.Sprintf("{\"errno\":%d, \"errmsg\":\"%s\", \"dataset\":\"%s\"\n", this.Errno, this.Errmsg, this.Dataset)
}

func (this *ScMessage) ScSend() bool {
	var scurl = strings.Replace(SCURL, "${SCKEY}", this.sckey, 1)
	resp, err := http.PostForm(scurl, url.Values{"text": {this.textData}, "desp": {this.despData}})
	defer resp.Body.Close()
	if err != nil {
		return false
	} else {
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return false
		} else {
			var scResp scRespone
			if err = json.Unmarshal(body, &scResp); err == nil {
				return scResp.Errmsg == "success"
			} else {
				return false
			}
		}
	}
}

type send struct {
	ScMessage
}

func (this *send) handler() error {
	var text, desp string
	var err error
	if pfile != "" {
		if ptext != "" {
			return errors.New(fmt.Sprintf("pfile and ptext param must be only one ."))
		} else {
			text, desp, err = readScInfoFile(pfile)
			if err != nil {
				return err
			}
		}
	} else {
		if ptext != "" {
			text = ptext
		} else {
			return errors.New(fmt.Sprintf("text param  must be not empty."))
		}
		if pdesp != "" {
			desp = pdesp
		}
	}
	if err = checkText(text); err == nil {
		this.textData = text
	} else {
		return err
	}
	if err = checkDesp(desp); err == nil {
		this.despData = desp
	} else {
		return err
	}

	if psckey != "" {
		this.sckey = psckey
	} else {
		if sckey, err := readsckey(); err == nil {
			this.sckey = sckey
		} else {
			return err
		}
	}
	rs := this.ScSend()
	if rs {
		fmt.Println(":) Send message ok.")
		return nil
	} else {
		return errors.New(fmt.Sprintf("(: Send message failed.\n"))
	}
}

type info struct {
}

func (this *info) handler() error {
	fmt.Println(INFO)
	return nil
}

type sckey struct {
}

func (this *sckey) handler() error {
	if psckey == "" {
		if sckey, err := readsckey(); err == nil {
			fmt.Printf("Your sc's sckey is : %s \n", sckey)
			return nil
		} else {
			return err
		}
	} else {
		err := writesckey(psckey)
		if err != nil {
			return errors.New(fmt.Sprintf("Update Your local %s sc's sckey failed.\n", sckeyfilePathName))
		} else {
			fmt.Printf("Your src's sckey update successful.\n")
			return nil
		}
	}
}

type help struct {
}

func (this *help) handler() error {
	flag.Usage()
	return nil
}
